﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace CSVToPy
{
    class ConvertException : Exception
    {
        public int ExitCode = -1;

        public ConvertException(int exitCode, string msg)
            : base(msg)
        {
            ExitCode = exitCode;
        }

        static public string BuildErrorDetail(string line, int pos, string msg = "")
        {
            ASCIIEncoding n = new ASCIIEncoding();
            byte[] b = n.GetBytes(line.Substring(0, pos));

            int l = 0; // String length in ascii character
            for (int i = 0; i <= b.Length - 1; i++)
            {
                if (b[i] == 63) 
                {
                    l++;
                }

                l++;
            }

            return msg + ":\n" + line + "\n" + new string(' ', l) + "^";
        }

        static public string BuildErrorDetail(int line_no, string msg)
        {
            return "Line " + line_no + ": " + msg;
        }

        static public string BuildErrorDetail(int line_no, string column_name, string msg)
        {
            return "Line " + line_no + ", Column name = " + column_name + ": " + msg;
        }
    }

    class Program
    {
        enum RowType { STRING, NUMBER, BOOLEAN, UNKNOWN }
        static readonly string PyHeader = "# -*- coding: UTF-8 -*-";
        static readonly string PyDictHeader = "data = {";
        static readonly string PyDictFooter = "}";
        static readonly string PyColon = " : ";
        static readonly string PyComma = ", ";
        static readonly string PySingleQuote = "'";
        static readonly string PyOpenBrace = "{";
        static readonly string PyCloseBrace = "}";
        static readonly string PyIndent = "\t";
        static readonly char CSVComma = ',';
        static readonly char CSVDoubleQuote = '"';
        static readonly string[] PySpecialMap = new string[256];
        public delegate string ReadLine();

        static void ShowHelp()
        {
            Console.WriteLine("Usage:");
            Console.WriteLine("CSVToPy.exe c:\\data.csv c:\\data.py");
        }

        static int NextIdxSkipWhitespace(string s, int idx)
        {
            while (idx < s.Length)
            {
                if (!char.IsWhiteSpace(s[idx]))
                {
                    return idx;
                }

                ++idx;
            }

            return -1;
        }

        static void ParsePhysicalLine(ref List<string> parts, ref StringBuilder sb, ref bool inQuotes, string line)
        {
            // Combine multi physical lines to a logical line.
            if (inQuotes)
            {
                sb.Append("\\n");
            }
            else
            {
                line = line.TrimStart();
            }

            for (int i = 0; i < line.Length; ++i)
            {
                char character = line[i];

                if (character == CSVComma)
                {
                    if (inQuotes)
                    {
                        sb.Append(character); // Current ',' is a string
                    }
                    else
                    {
                        parts.Add(sb.ToString().Trim());
                        sb.Remove(0, sb.Length);
                        int idx = NextIdxSkipWhitespace(line, i + 1);

                        if (idx != -1)
                        {
                            i = idx - 1;
                        }
                        else
                        {
                            i = line.Length - 1;
                        }
                    }
                }
                else if (character == CSVDoubleQuote)
                {
                    if (inQuotes)
                    {
                        if (i + 1 < line.Length)
                        {
                            if (line[i + 1] == CSVDoubleQuote)
                            {
                                sb.Append(character); // Current '""' is a '"'
                                ++i;
                                continue;
                            }
                            else
                            {
                                int idx = NextIdxSkipWhitespace(line, i + 1);

                                if (idx != -1 && line[idx] != CSVComma)
                                {
                                    string errMsg = ConvertException.BuildErrorDetail(line, idx, "Expecting ','");
                                    throw new ConvertException(2, errMsg);
                                }
                            }
                        }

                        inQuotes = false;
                    }
                    else
                    {
                        if (sb.Length != 0)
                        {
                            string errMsg = ConvertException.BuildErrorDetail(line, i, "Expecting not a '\"'");
                            throw new ConvertException(2, errMsg);
                        }

                        inQuotes = true;
                    }
                }
                else
                {
                    if (char.IsControl(character) || character == '\'' || character == '\\')
                    {
                        sb.Append(PySpecialMap[(uint)character]);
                    }
                    else
                    {
                        sb.Append(character);
                    }
                }
            }
        }

        static string[] ParseLine(ReadLine readLine)
        {
            List<string> parts = new List<string>();
            StringBuilder sb = new StringBuilder(100);

            bool inQuotes = false;

            do
            {
                string line = readLine();

                if (line == null) break; // Break null line, maybe reach the end or finish a logic line.

                ParsePhysicalLine(ref parts, ref sb, ref inQuotes, line);
            } while (inQuotes);

            if (inQuotes)
            {
                throw new ConvertException(2, "Expecting '\"' at the end of csv");
            }
            else if (parts.Count != 0 || sb.Length != 0)
            {
                parts.Add(sb.ToString().Trim());
                return parts.ToArray();
            }

            return null; // Reach the end of csv
        }

        static bool IsNumber(string part)
        {
            double d;

            return part.Count() > 0 && (char.IsDigit(part[0]) || part[0] == '+' || part[0] == '-') && double.TryParse(part, out d);
        }

        static bool IsBool(ref string part)
        {
            if (string.Compare(part, "True", true) == 0)
            {
                part = "True";
                return true;
            }

            if (string.Compare(part, "False", true) == 0)
            {
                part = "False";
                return true;
            }

            return false;
        }

        static string BuildItem(int line_no, ref RowType rowType, string key, string val, string[] buffer)
        {
            // key is string.
            bool isKeyNumber = IsNumber(key);
            bool isValNumber = IsNumber(val);
            bool isBool = IsBool(ref val);

            // check column data type.
            if (rowType == RowType.UNKNOWN)
            {
                if (isValNumber)
                {
                    rowType = RowType.NUMBER;
                }
                else if (isBool)
                {
                    rowType = RowType.BOOLEAN;
                }
                else
                {
                    rowType = RowType.STRING;
                }
            }
            else
            {
                if (isValNumber && rowType != RowType.NUMBER)
                {
                    string errMsg = ConvertException.BuildErrorDetail(line_no, key,
                        "Column data have inconsistent types, " + val + " has type " + RowType.NUMBER.ToString() + ", but expecting type " + rowType.ToString() + ".");
                    throw new ConvertException(4, errMsg);
                }
                else if (isBool && rowType != RowType.BOOLEAN)
                {
                    string errMsg = ConvertException.BuildErrorDetail(line_no, key,
                        "Column data have inconsistent types, " + val + " has type " + RowType.BOOLEAN.ToString() + ", but expecting type " + rowType.ToString() + ".");
                    throw new ConvertException(4, errMsg);
                }
                else if (!isValNumber && !isBool && rowType != RowType.STRING)
                {
                    string errMsg = ConvertException.BuildErrorDetail(line_no, key,
                        "Column data have inconsistent types, " + val + " has type " + RowType.STRING.ToString() + ", but expecting type " + rowType.ToString() + ".");
                    throw new ConvertException(4, errMsg);
                }
            }

            if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(val))
            {
                return string.Empty;
            }
            else
            {
                buffer[0] = isKeyNumber ? string.Empty : PySingleQuote;
                buffer[1] = key;
                buffer[2] = isKeyNumber ? string.Empty : PySingleQuote;
                buffer[3] = PyColon;
                buffer[4] = (isValNumber || isBool) ? string.Empty : PySingleQuote;
                buffer[5] = val;
                buffer[6] = (isValNumber || isBool) ? string.Empty : PySingleQuote;

                return string.Join(string.Empty, buffer);
            }
        }

        static string BuildItem(string key, string[] vals, string[] buffer)
        {
            bool isNumber = IsNumber(key);
            string val = string.Join(PyComma, vals);// all vals are string.

            if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(val))
            {
                return string.Empty;
            }
            else
            {
                buffer[0] = isNumber ? string.Empty : PySingleQuote;
                buffer[1] = key;
                buffer[2] = isNumber ? string.Empty : PySingleQuote;
                buffer[3] = PyColon;
                buffer[4] = PyOpenBrace;
                buffer[5] = val;
                buffer[6] = PyCloseBrace;

                return string.Join(string.Empty, buffer);
            }
        }

        static string BuildLine(int line_no, string[] columns, string[] parts)
        {
            if (columns.Count() == parts.Count() && columns.Count() > 0)
            {
                string[] buffer = new string[7];
                List<string> lstVals = new List<string>();
                RowType currRowType = RowType.UNKNOWN;

                for (int idx = 1; idx < columns.Count() && idx < parts.Count(); ++idx)
                {
                    string item = BuildItem(line_no, ref currRowType, columns[idx], parts[idx], buffer);

                    if (!string.IsNullOrEmpty(item))
                    {
                        lstVals.Add(item);
                    }
                }

                string line = BuildItem(parts[0], lstVals.ToArray(), buffer);

                if (!string.IsNullOrEmpty(line))
                {
                    return PyIndent + line + PyComma;
                }
            }
            else
            {
                string errMsg = ConvertException.BuildErrorDetail(line_no, "Column count is zero or part count mismatch column count.");
                throw new ConvertException(3, errMsg);
            }

            return string.Empty;
        }

        static int Main(string[] args)
        {
            if (args.Count() != 2)
            {
                ShowHelp();
                return 0;
            }

            HashSet<string> setPK = new HashSet<string>();
            HashSet<string> setSubPK = new HashSet<string>();
            StringBuilder sbPy = new StringBuilder(100);

            sbPy.AppendLine(PyHeader);
            sbPy.AppendLine(PyDictHeader);

            PySpecialMap[(uint)'\''] = "\\'";
            PySpecialMap[(uint)'\\'] = "\\\\";
            PySpecialMap[(uint)'\a'] = "\\a";
            PySpecialMap[(uint)'\b'] = "\\b";
            PySpecialMap[(uint)'\f'] = "\\f";
            PySpecialMap[(uint)'\t'] = "\\t";
            PySpecialMap[(uint)'\v'] = "\\v";

            try
            {
                using (StreamReader sr = new StreamReader(args[0], Encoding.GetEncoding("gb18030")))
                {
                    string[] columns = null;
                    string[] parts = null;
                    int line_no = 0;

                    while ((parts = ParseLine(sr.ReadLine)) != null)
                    {
                        ++line_no;

                        if (parts.Count() > 0)
                        {
                            if (setPK.Contains(parts[0]))
                            {
                                string errMsg = ConvertException.BuildErrorDetail(line_no, "Row index PK conflict - '" + parts[0] + "'");
                                throw new ConvertException(1, errMsg);
                            }

                            // Init column list, column type list.
                            if (columns == null)
                            {
                                columns = parts;
                                for (int idx = 0; idx < columns.Length; ++idx)
                                {
                                    // check column index pk.
                                    if (setSubPK.Contains(columns[idx]))
                                    {
                                        string errMsg = ConvertException.BuildErrorDetail(line_no, "Column index PK conflict - '" + columns[idx] + "'");
                                        throw new ConvertException(1, errMsg);
                                    }
                                    else
                                    {
                                        setSubPK.Add(columns[idx]);
                                    }
                                }
                            }
                            else
                            {
                                string linePy = BuildLine(line_no, columns, parts);

                                if (!string.IsNullOrEmpty(linePy))
                                {
                                    setPK.Add(parts[0]);
                                    sbPy.AppendLine(linePy);
                                }
                            }
                        }
                    }
                }
            }
            catch (ConvertException e)
            {
                Console.WriteLine(e.Message);
                return e.ExitCode;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return 2;
            }

            sbPy.AppendLine(PyDictFooter);

            try
            {
                using (StreamWriter sw = new StreamWriter(args[1], false, Encoding.GetEncoding("UTF-8")))
                {
                    sw.Write(sbPy.ToString());
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return 2;
            }

            return 0;
        }
    }
}
